結論先說:histogram 埋對了只是起點,dashboard 要先讓值班的人看到「使用者受影響多少」,AI workflow 裡一次次呼叫疊起來的尾端延遲,往往比任何單一服務的 P99 都危險。
上篇(Day 16 上)拆解了「平均值為什麼會騙人」:P95 不等於保證 95%、percentile 有 nearest-rank 與內插兩種算法、並動手在 FastAPI 用 Prometheus histogram 記下完整分布,而不是只印一個平均數。下篇接著把這份分布資料變成值班可用的 dashboard,並拆穿幾個常見誤判。
Day16 的 dashboard 不需要一口氣放滿 GPU、CPU、每個 provider 和每一條 query。
先做一個由外向內的版面:
row 1 使用者體驗:request rate、P50、P95、P99、成功比例
row 2 workflow:queue delay、retrieval、TTFT、generation、tool latency
row 3 依賴:provider error、vector DB duration、tool API duration
row 4 資源:worker saturation、connection pool、CPU / GPU utilization
把這個版面翻譯成一張可以直接貼進 Grafana 的 panel 骨架,示意每一列大概放什麼查詢:
{
"title": "Row 1 - User Experience",
"panels": [
{"title": "Request Rate", "expr": "sum(rate(ask_request_duration_seconds_count[5m]))"},
{"title": "P50", "expr": "histogram_quantile(0.50, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
{"title": "P95", "expr": "histogram_quantile(0.95, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
{"title": "P99", "expr": "histogram_quantile(0.99, sum by (le) (rate(ask_request_duration_seconds_bucket[5m])))"},
{"title": "Success Ratio", "expr": "sum(rate(ask_request_duration_seconds_count{workflow_status=\"completed\"}[5m])) / sum(rate(ask_request_duration_seconds_count[5m]))"}
]
}
這不是一份要照抄貼上就能跑的完整 Grafana provisioning 檔,而是示意結構:第一列的每個 panel 都是使用者視角能看懂的問題,不是「CPU 用了多少」。
第一列先問「誰在等」。
最後一列才問「哪台機器在喘」。
這個順序能避免把 CPU 70% 直接等同於使用者體驗差,也避免看到 P95 升高就立刻擴容。
假設 14:20 的 P95 從 1.2 秒跳到 4.8 秒:
request_id、trace_id 找少數慢樣本,確認 prompt、model route、文件數與工具呼叫。這套順序刻意從使用者影響開始。
直接從 CPU、記憶體或某一筆 exception 開始,很容易修到一個剛好存在、卻不是這次 tail latency 的症狀。
抽象的七步驟不容易記,走一次具體場景會清楚很多。以下是一個依照本文設計出來的假想值班情境,用來示範這套判讀順序實際怎麼用,不是真實 incident 記錄。
時間 14:20,值班的 on-call 工程師收到告警:/ask 的 P95 從平常的 1.2 秒跳到 4.8 秒。
14:20 告警觸發:p95_latency_seconds{route="/ask"} > 3
14:21 第一步:看 request count
過去 5 分鐘有 340 筆請求,樣本數足夠,排除單筆離群值撐起整條線的可能
14:22 第二步:看 P50
P50 從 180ms 只微升到 210ms,P95/P99 卻大幅上升
→ 初步判斷:多數請求正常,問題集中在尾端
14:24 第三步:看 workflow breakdown
retrieval_ms、queue_ms 平穩
ttft_ms 的 P95 從 300ms 上升到 3.9 秒
generation_ms 平穩
→ 問題縮小到「拿到第一個 token 之前」這一段
14:26 第四步:看 error / fallback / timeout
5xx 沒有明顯上升,workflow_status=completed 比例正常
→ 這不是錯誤事件,是「慢但沒壞」
14:29 第五步:用 trace_id 撈幾筆慢樣本
發現這些慢請求的 model_route 都指向同一個 provider endpoint
14:31 第六步:看部署 annotation
13:55 有一筆變更:把預設 model_route 從 primary 切到 secondary(灰度測試)
14:33 第七步:選擇 mitigation
把灰度流量切回 primary,觀察 TTFT 是否回落
14:41 P95 回到 1.3 秒附近,宣告緩解
這個場景刻意設計成「沒有任何一段 CPU 或記憶體出問題」,純粹是流量被切到一個排隊比較嚴重的 provider endpoint。如果值班工程師一開始就直奔資源使用率的 dashboard,大概率會看到一片正常的 CPU 曲線,然後陷入「明明資源正常,為什麼會變慢」的困惑——這正是 ⑦ 段開頭強調「先問誰在等,再問哪台機器在喘」的原因:資源正常,不代表使用者體驗正常;體驗變差的根因,經常藏在應用層的路由、佇列或第三方依賴,而不是基礎設施層。
傳統同步 API 已經會受 dependency tail latency 影響。
AI workflow 又常將多個步驟串起來:
retrieve
+ rerank
+ model TTFT
+ model generation
+ optional tool call
+ validation
每一段都有自己的分布。
如果每個階段都只在 P95 看起來「還可以」,端到端 tail 仍可能不可接受。
舉例來說,retrieval P95 400 ms、TTFT P95 1.2 秒、generation P95 2 秒、tool P95 1 秒,不能直接相加後就宣稱端到端 P95 是 4.6 秒;慢事件是否同時發生取決於相關性。
但這份加總足以提醒我們:別讓每個 team 都只優化自己的局部平均值,最後由使用者承擔整條鏈的尾端。
這個現象值得單獨講一次,因為它在組織裡特別容易發生。
retrieval team 的 OKR 是「P95 維持在 400ms 以下」,他們達標了。
model serving team 的 OKR 是「TTFT P95 維持在 1.2 秒以下」,他們也達標了。
tool team 的 OKR 是「tool call P95 維持在 1 秒以下」,同樣達標。
三個 team 的季度回顧都寫著「本季 SLO 全部達標,服務品質穩定」。
但使用者感受到的是這三段疊在一起的端到端體驗。
如果三段慢請求剛好落在同一次請求裡的機率不算低,使用者的端到端 P95 完全可能遠超過任何一個 team 自己回報的數字。
這不是任何一個 team 說謊,是組織把一個端到端的使用者體驗問題,拆成了互不相干的局部指標。
解法不是取消各 team 的局部 SLO,而是在它們之上,額外保留一條端到端的 SLI,由沒有局部利益的角色(例如平台團隊或 SRE)負責盯著。
Day17 會正式把這種端到端 SLI 的定義方式講清楚。
當 P95 突然上升,常見反應是「多塞一點文件給模型」。
這可能同時拉高 prompt token、TTFT、成本與 parser 壓力。
更多 context 有時提高 groundedness,有時只增加噪音;它是 evaluation 問題,也是 latency 問題。
比較兩個 retrieval 設定時,請至少並排看:
| 面向 | 設定 A | 設定 B | 要問的問題 |
|---|---|---|---|
| retrieved documents | 3 | 12 | 多出來的文件真的有用嗎? |
| input tokens | 較少 | 較多 | TTFT 是否被拉長? |
| P95 end-to-end | 較短 | 較長 | 使用者 deadline 是否仍成立? |
| groundedness evaluation | 待量測 | 待量測 | 品質有沒有換到可證明的改善? |
| token cost | 較低 | 較高 | 每個成功任務的成本是否合理? |
數字欄位故意寫「待量測」。
沒有跑過固定資料集和相同流量條件,不要把「文件更多」說成品質更好,也不要把「P95 更短」說成最佳設計。
這張表刻意把 latency 欄位和 groundedness、cost 欄位放在同一張表裡,是想強調一個常被拆開來看、但其實密不可分的事實:retrieval 設定的每一個改動,幾乎都同時是延遲問題、品質問題、成本問題三者疊在一起的決策。
只從 latency 團隊的角度看,會傾向把 TopK 調小,讓 P95 好看;只從 evaluation 團隊的角度看,可能傾向把 TopK 調大,追求更高的 groundedness 分數;只從財務角度看,兩邊都嫌貴。
如果三個團隊各自拿著自己的那個欄位去做決策,很容易在不同季度來回搖擺,調大又調小,卻從未真正解決任何一方的問題。
比較健康的做法,是像這張表一樣,把三個維度攤在同一張表格裡,一起看,一起決定要往哪個方向妥協——這也呼應⑧段稍早那個「每個 team 都達標,使用者卻不滿意」的教訓:局部最佳化,經常是全域次佳的來源。
慢請求最危險的修法之一,是沒有上限地重試。
若 provider 原本已擁塞,client timeout 後再送一次,不只延長那位使用者的等待,還替 provider 增加一個新工作。
這不是紙上談兵。DoorDash 在 2021 年 6 月 19 日的一次正式 postmortem 中,公開描述了幾乎一模一樣的機制:16:30 PDT,payment 基礎設施開始出現高延遲,Dasher App 依賴的下游呼叫回應時間拉長;16:35 系統告警觸發、工程師被 page。讓事故真正升級的不是延遲本身,而是接下來發生的事——Dasher 端系統偵測到請求變慢或逾時後,開始對本已不健康的 payment 服務發起額外重試,這些重試流量疊加在原本就吃緊的基礎設施上,形成經典的正回饋迴圈:延遲越高、重試越多;重試越多、延遲越高。事故一路升級到 17:19 PDT 被迫暫停顧客下新單、17:22 對 Drive 合作夥伴做同樣處置止血,真正的轉折點在 18:25——團隊靠設定變更阻斷了下游 payment 呼叫,而不是靠更聰明的重試策略,才讓系統有空間喘息、逐步恢復。從 16:30 到 18:36,多數 Dasher 完全無法接單或上線,事故總長超過兩小時。這個案例的重點不是「DoorDash 的重試邏輯寫得差」,而是提醒我們:沒有上限、沒有 circuit breaker 概念的重試,在系統已經處於高延遲狀態時,止血手段往往是「先讓下游呼叫停下來」,而不是「更努力地重試」。
AI 呼叫還會增加 token 成本;每次 retry 都可能重新送入 prompt。
「每次 retry 都可能重新送入 prompt」講起來是一句話,但把它換算成具體數字,才會知道這件事在 AI workflow 裡有多昂貴。假設一次 /ask 呼叫的 prompt(含檢索回來的文件片段)平均是 3,000 input tokens,輸出平均是 500 output tokens,用一個常見的定價量級估算(每百萬 input tokens 數美元、output tokens 通常是 input 的數倍價格),一次呼叫的成本可能落在幾分錢美金的等級——單看很便宜。但把 retry 疊上去:
正常路徑(無 retry)
1 次呼叫 = 3,000 input + 500 output tokens = 成本 X
client timeout 後重試一次(無上限 retry 的情境)
第 1 次:3,000 input + 500 output(provider 端可能仍在處理,只是 client 沒等到)
第 2 次:3,000 input + 500 output(重新送出同一個 prompt)
= 至少 2 倍的 token 成本,而且 provider 端可能同時在處理兩份幾乎一樣的工作
如果重試策略沒有上限,且觸發條件過於寬鬆(例如任何 timeout 都無條件重試 3 次),一個流量高峰期間的 provider 延遲,可以在幾分鐘內把 token 成本推高到平常的數倍——這還沒算上 DoorDash 案例裡真正致命的那件事:這些重試流量疊加在已經吃緊的 provider 之上,讓延遲更嚴重,觸發更多 timeout,形成 ⑧ 段開頭那句「延遲越高、重試越多;重試越多、延遲越高」的正回饋迴圈。傳統 Web API 的 retry 風暴,代價主要是「更多連線」和「更長的排隊」;AI workflow 的 retry 風暴,多了一層「每次重複都在燒真金白銀的 token 預算」,這是為什麼 AI 系統的 retry policy 不能直接沿用傳統系統的預設值,需要額外把成本預算算進去。
可以先把 retry policy 寫成可閱讀的 contract:
only retry: connection reset、temporary 429、明確可重試的 5xx
do not retry: invalid request、policy denial、parser contract failure
max attempts: 2
timeout: bounded by the user-facing deadline
backoff: exponential with jitter
record: attempt number, retryable reason, final outcome
這不是萬用設定。
它要求你在寫 retry 前回答:原本請求是否可能已被 provider 執行?重送是否具冪等性?剩餘 deadline 還夠不夠?成本預算是否允許?
Day29 會回來把 timeout、backoff、circuit breaker 和 degradation 放到同一個故障模型中;今天先不要用 retry 把一張慢圖遮掉。
讀者讀到這裡,可能已經在腦中同時裝了 hedged requests、retry、timeout、circuit breaker 好幾個工具,容易混淆什麼時候該用哪一個。
先講清楚它們各自解決的問題不同,不是互相替代的關係。
下游偶爾、獨立地變慢(stragglers)
→ hedged requests:用多打一份請求換取「取先到的那個」
下游暫時失敗、值得再試一次
→ bounded retry:有上限、有 backoff、確認冪等性
下游已知會花多久、你只是要設一個放棄的底線
→ timeout:不是用來讓下游變快,是用來保護自己的資源
下游持續、系統性地故障,重試只會讓情況更糟
→ circuit breaker:先暫停呼叫,給下游喘息空間,自己也切到 fallback
四個工具分別對應四種不同的下游行為模式,用錯地方,效果適得其反:對一個持續故障的下游用 hedged requests,等於把負荷直接翻倍打上去,加速它的崩潰;對一個系統性故障的下游只調大 timeout,只是讓使用者等更久才等到同一個失敗結果。
Day29 會把這四個工具放進同一張決策表,搭配明確的判斷條件;今天先記住:它們是針對不同故障模式設計的,不是「效果差不多、選一個順手的就好」的同義詞。
這一段整理的五個誤判,有一個共同的心理機制:它們都是「看到一個數字改善了,就停止追問」的結果。
percentile、histogram、breakdown log 這些工具,存在的目的不是取代判斷,而是讓「停止追問」這個動作,多一道有證據支撐的關卡。
以下每個誤判,都是本文前面某個概念被略過之後,實際會發生的具體後果。
新版模型把 95% request 從 700 ms 降到 500 ms,卻讓 5% request 從 2 秒變成 20 秒。
mean 可能仍下降。
若使用者恰好落在那 5%,他不會因為其他人變快就覺得自己被服務得很好。
用具體數字看這個陷阱有多容易發生:
舊版本:100 筆請求,全部落在 650-750ms 之間
mean = 700ms
新版本:100 筆請求
95 筆落在 480-520ms(改善明顯)
5 筆落在 19,000-21,000ms(某個 edge case 觸發重試循環)
mean = 95×500ms/100 + 5×20,000ms/100 = 475 + 1,000 = 1,475ms
咦,這個算法下新版本 mean 反而更差,看起來很容易被抓到。
問題在於很多團隊比較的不是逐筆重算的 mean,而是儀表板上滾動窗口的移動平均——如果那 5% 的極端值恰好被平滑掉,或是被同一時間窗內大量的正常請求稀釋,滾動平均線很可能還是往下走的。
這正是為什麼「附上 count、P50、P95、P99」不是嚴謹主義的形式要求,而是唯一能攔住這種誤判的辦法:任何一個「變快了」的結論,都要能同時交叉看到分布沒有被破壞,而不是只看一條平滑過的線在往下掉。
每次宣稱「變快」至少附上 count、P50、P95、P99、時間窗、request class 與版本資訊。
P99 上升可能是 worker 飽和,也可能是單一 provider 請求卡住、GC pause、DNS 重試、過長 prompt 或低樣本。擴容也許有用,但它不是解釋——先用 trace 找共同特徵,再決定是否把錢花在更多 worker、更多 GPU、provider 路由或更小的 context。
一個常見的反例:P99 上升,團隊立刻加開兩倍 worker,P99 卻紋風不動。事後查才發現,瓶頸根本不在 worker 數量,而是所有 worker 都在排隊等同一個外部 provider 的 API 配額——worker 越多,只是讓越多請求同時卡在等待配額的狀態,佇列變得更長而不是更短,還多花了運算資源的錢,錢燒在了不會解決問題的地方。先看 trace 裡「時間花在哪一段」,比先看「哪個資源使用率比較高」更接近真正的瓶頸。
較大的 timeout 會減少「timeout error」,卻不會讓下游更快,可能讓 queue 更長、佔住 connection、延後 fallback、讓使用者等更久。timeout 是 deadline contract 的一部分;變更前要看 end-to-end latency、併發、retry 行為與使用者能接受的等待時間。
把 timeout 想成一個「你願意讓使用者等多久,系統才主動放棄」的承諾——調大它不會讓下游變快,只是把「放棄的時間點」往後推。在被推後的這段時間裡,原本那個 connection、worker、配額都被佔住,無法服務其他請求;如果同時有更多使用者送出新請求,佇列會因為舊請求遲遲不肯放手而變得更長,更長的佇列又意味著更多新請求要等更久才輪到自己。這是另一種正回饋迴圈,跟 ⑧ 段講的 retry 風暴同樣的機制:一個試圖止血的設定,反而讓失血速度加快。正確的順序是先問「這個下游本身該花多久」,而不是先問「要設多大 timeout 才不會跳錯誤」。
十筆請求裡的 P99,沒有足夠樣本支撐精細的外部承諾。先累積足夠資料,或改用較適合低流量服務的評估方式,例如 synthetic check、request count guardrail 與案例測試。SLA 是對外責任,不是 dashboard 上一個看起來專業的數字。
用數字說明這有多不可靠:nearest-rank 算法下 ceil(0.99 × 10) = 10,十筆裡的 P99 就是排序後最後一筆——本質上等於「這批樣本裡最慢的那一筆」,跟其餘九筆完全沒有關係。只要剛好有一筆請求因冷啟動、GC 或任何偶發原因變慢,P99 就會直接反映那唯一一筆,跟系統整體表現的關聯性微乎其微。要讓 P99 具備統計意義,得有足夠樣本讓「第 99 百分位」真的代表一群請求裡的某個位置,而不是單獨一筆的巧合。對流量本來就低的內部工具或早期產品,與其硬套 P99 SLA,更誠實的做法是先用 synthetic check 搭配 request count guardrail(明確標示樣本數低於門檻時僅供參考),等流量成長到可信的量級,再談外部 SLA 承諾。
使用者已關閉頁面,server 仍完成了 30 秒生成;後端可能把它記為 successful request。server-side latency 仍有價值,但它不能替代 user journey metric。若收集 client telemetry,必須先處理 consent、資料最小化與身份資訊,不要為了做漂亮圖表把敏感 prompt 一起送走。
這個誤判特別容易發生在串流場景:使用者看到前幾個 token 就覺得「有反應了」,切走分頁去做別的事,但 server 端不知道使用者已離開,連線技術上還開著,繼續把剩下的 token 一個個生成、送出。30 秒後生成完畢,伺服器把這筆請求記成 workflow_status=completed,histogram 也正常記了一筆 30 秒的 observation——從系統角度看是一次成功、只是比較慢的請求;從使用者角度看,他根本沒等到答案。要抓到這種落差,通常需要偵測連線是否仍存活(例如 FastAPI 的 request.is_disconnected()),把「client 已斷線」標記成獨立的 workflow_status,而不是塞進 completed 或 failed 裡混淆視聽。多花的那 30 秒運算資源與 token 成本也是實際發生的浪費——即使使用者從未見到結果,provider 帳單一樣照算。
團隊常常同時做兩件事:加大 retrieval 的文件數量、同時觀察到 P95 上升,卻把這兩件事當成兩條獨立、需要分開排查的線索。實際上它們經常是同一件事:更多文件代表更多 input tokens,更多 input tokens 直接拉長 TTFT(模型需要先處理完整個 prompt 才能吐出第一個 token),也可能拉長 generation(更長的上下文有時讓模型輸出也變長)。這不是巧合,是 ⑧ 段「context 越大,不等於答案越可靠」那張對比表想強調的因果鏈——如果你剛好在同一週做了「加大 retrieval TopK」和「調整 timeout 設定」兩件事,P95 上升時,請先檢查有沒有部署 annotation 對到「加大 TopK」的那次變更,而不是先假設是 timeout 設定出了問題。
| 誤判 | 表面現象 | 真正該檢查的東西 |
|---|---|---|
| 只看 mean 宣布變快 | 平均值下降 | P50/P95/P99 是否同時下降,還是被少數樣本拉動 |
| 看到 P99 就擴容 | P99 上升 | 是資源飽和,還是單一依賴、GC pause、低樣本 |
| 把 timeout 調大 | timeout error 減少 | end-to-end latency、佇列長度是否同時惡化 |
| 用低流量 P99 寫 SLA | 樣本數不足 | 是否累積了足夠請求量再承諾外部指標 |
| 只看 service 成功 | 忽略 client cancellation | server-side 完成 ≠ 使用者真的收到結果 |
| context 變大與 P95 變長視為巧合 | 兩個獨立變更同時發生 | 部署時間軸是否指向同一次變更 |
這張表格的共同結論是:每一個誤判的根源,都是把「看到的現象」直接當成「發生的原因」,跳過了中間本該做的交叉檢查。tail latency 的排查,幾乎每一步都需要至少兩個獨立訊號互相印證(P50 與 P95 一起看、latency 與 error rate 一起看、metric 變化與部署時間軸一起看),單一訊號幾乎永遠不足以下結論。
以下是讀者自行執行 Lab 後可勾選的驗收項目。
這份清單每一項都對應本文某個曾經講過的教訓:第一項對應 ①、⑤ 段「percentile 描述的是一個已定義好的母體」;第二到四項對應⑤段「一致的母體」,避免混進不同流量或高基數 label;第五、六項對應 ②、⑥ 段的 DIY 精神;第七項對應④段「一句『很慢』不夠拿來排障」;第八項對應⑥段步驟 5「histogram_quantile 是估算」;第九項對應④段 latency contract 與⑦段判讀流程;第十、十一項對應⑨段的常見誤判。如果讀者跑完 Lab 回頭勾不完這十一項,不代表 Lab 失敗——它剛好標出了下一步該補強的地方。
本文的 injection 只是在 application 內加上可預期的等待。
它不會模擬真實 provider 的 token streaming、網路抖動、排隊理論、connection pool 耗盡或 GPU VRAM 壓力。
它的價值是把「平均值看起來正常」這個錯覺變成可重跑的反例。
老實列出差距,比假裝這個 Lab 已經涵蓋一切更有用。
| Lab 練習 | 真實 production | 差距的影響 |
|---|---|---|
asyncio.sleep() 固定延遲 |
provider 端排隊、GPU 搶佔、真實網路抖動 | 真實延遲的分布形狀比固定值複雜得多 |
| 單機、單 worker | 多副本、跨可用區、負載平衡 | 需要額外考慮 hedge、跨副本聚合 |
| 20 筆手動送出的請求 | 每秒數百到數千筆的真實流量 | percentile 的統計意義完全不同 |
| 沒有真實下游依賴 | database、cache、第三方 API 各自有自己的分布 | 端到端 latency 是多個分布的疊加 |
| 沒有並發控制 | connection pool、rate limit、backpressure | 高並發下的排隊效應在 Lab 裡看不到 |
| 一次性測試 | 全天候、跨時區的流量波動 | 需要看不同時間窗下 percentile 是否穩定 |
這張表不是要讀者氣餒,而是提醒:本文的 DIY 只解決「我有沒有能力量到分布」這個第一層問題。
第一層問題沒解決之前,後面的排隊理論、hedge、跨副本聚合都無從談起。
這也是為什麼 SRE 的能力養成常常是階梯式的:先確保看得見,才談得上優化。
除了 ⑥ 段步驟 4.5 提到的 scrape target 檢查,還有幾個更隱蔽的可能性值得排除。第一,bucket 邊界沒有涵蓋你注入的延遲值——如果 buckets 只設到 1 秒而你注入 500ms,這筆請求會被歸進 le="1" 這個桶,不會單獨顯示成異常。第二,label 基數爆炸讓某些 series 被 Prometheus 丟棄——如果不小心把 request_id 當成 label(⑥ 段步驟 1 警告過這件事),可能觸發 sample limit,悄悄丟掉部分資料點。第三,middleware 或 reverse proxy 層有自己的 timeout,請求根本沒走到你埋 histogram 的那段程式碼——這種情況下 Prometheus 端看起來完全正常,因為被攔截的請求根本沒機會被計入。排查時先問「這筆慢請求有沒有真的執行到我埋 metric 的那一行」,比懷疑 Prometheus 設定錯誤更快找到答案。
若 P95 沒有隨注入延遲上升,先不要假設 Prometheus 壞了,依序確認:
這個順序能把「測量鏈路壞了」與「服務沒有變慢」分開——排查量測本身是否可信,本來就得沿著量測鏈路走一遍。
下一篇會把 Day14 的 SLO spec、Day15 的 availability classifier 和今天的 latency histogram 合在一起,建立不會混淆使用者旅程與機器健康度的 FastAPI SLI。
到了 Day16,可以回頭把幾個看似分開的主題,串成一條線。
Day3 講故障注入:讓延遲、500、DB 不可用變成可重現的事件,而不是只能等它自然發生。
Day16 延續同一種精神,只是把「注入」的對象,從整個 request 換成「特定比例的延遲」——目的一樣,都是把抽象的風險變成可觀察、可重跑的具體現象。
Day14 講 SLO spec:先把「合格」定義清楚,才能談有沒有達標。
Day16 ④ 段那份 latency contract,用的正是同一套語言——request class、valid request、completion、exclusions,跟 Day14 的 SLO spec 骨架幾乎一模一樣,只是把 latency 當成觀察對象。
Day15 講 availability:用 good / valid 這種比例定義使用者體驗,而不是用技術指標。
Day16 講的 P95、P99,本質上也是同一種思路的延伸:不是問「系統有沒有壞」,而是問「使用者這次的等待,落在整個分布的哪個位置」。
三天的內容合起來,回答的其實是同一個更大的問題:「使用者體驗好不好」該怎麼被量化、被觀察、被驗證,而不是各自獨立的三個知識點。
這條線會在 Day17 正式收斂:把 availability(Day15)、latency percentile(Day16)、dependency 健康度,一起塞進一組完整的 SLI 定義裡。
回到文章一開始那個數字:九十五個請求各 100ms,五個各 10 秒,平均約 595ms。
這篇文章從頭到尾,其實都在拆解「平均約 595ms」這句話裡藏著的問題。
它藏著一個計算方法的問題:percentile 該用 nearest-rank 還是內插,樣本數夠不夠支撐一個可信的數字。
它藏著一個定義範圍的問題:量的是哪一段(server-side、TTFT、還是 end-to-end)、哪些流量該被排除。
它藏著一個組織責任的問題:各 team 局部達標,不等於使用者端到端滿意,需要有人盯著加總後的體驗。
它也藏著一個工程選擇的問題:尾端延遲不是只能忍受的天災,Google、Discord、Netflix 的案例都證明,它是可以被拆解、被投資、被系統性改善的目標。
平均值適合看總體成本,尾端才會告訴你誰被卡住。下一篇把 availability、latency 和 dependency 組成一組可用的 FastAPI SLI。
Day 16 把平均值會騙人的機制拆開來看:分位數才知道最慢那群人在等多久。Day 17 接著把 availability、latency 跟 dependency 湊成一組可用的 FastAPI SLI,重點是 SLI 要貼近使用者體驗,不是貼近基礎設施指標。
這篇是 Learning SRE for the AI Era 系列的一部分。
Build → Trace → Break → Measure → Evaluate → Recover → Improve.